﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace PoolKit
{
    internal class CommandProcessorThreadPoolBased : ICommandProcessor
    {
        private readonly object _stateChangeCriticalSection = new object();
        private bool _disposed;
        private int _enqueuedCommandsCount;
        private readonly HashSet<RegistrationHandle> _registrationHandles = new HashSet<RegistrationHandle>();
        private readonly WaitHandle _neverSignallingHandle = new ManualResetEvent(false);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="commandToEnqueue"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public void EnqueueCommand(Command commandToEnqueue)
        {
            lock (_stateChangeCriticalSection)
            {
                ThrowIfDisposed();

                if (commandToEnqueue == null) throw new ArgumentNullException("commandToEnqueue");

                QueueCommandToPool(commandToEnqueue);
            }
        }

        public void RegisterCommandWaitingForHandleSignal(WaitHandle handleToWaitOn, Command commandToRunWhenHandleSignals)
        {
            RegisterCommandWaitingForHandleSignal(
                handleToWaitOn,
                commandToRunWhenHandleSignals,
                TimeSpan.FromMilliseconds(Timeout.Infinite));
        }

        private void RegisterCommandWaitingForHandleSignal(
            WaitHandle handleToWaitOn,
            Command commandToRunWhenHandleSignals,
            TimeSpan maximumTimeToWait)
        {
            lock (_stateChangeCriticalSection)
            {
                ThrowIfDisposed();

                if (handleToWaitOn == null) throw new ArgumentNullException("handleToWaitOn");
                if (commandToRunWhenHandleSignals == null) throw new ArgumentNullException("commandToRunWhenHandleSignals");

                var registrationHandle = new RegistrationHandle();

                AddRegistrationHandle(registrationHandle);

                try
                {
                    registrationHandle.SetHandle(ThreadPool.UnsafeRegisterWaitForSingleObject(
                                handleToWaitOn,
                                delegate(object parameter, bool timedOut)
                                {
                                    // Note: it's not obvious that when this code runs, it has successfully called WaitOne() on the wait handle.
                                    // It is assumed that the command being executed knows the handle and will release the handle when it finishes running.

                                    try
                                    {
                                        commandToRunWhenHandleSignals.Execute();
                                    }
                                    finally
                                    {
                                        registrationHandle.Unregister();

                                        RemoveRegistrationHandle(registrationHandle);
                                    }
                                }, null, maximumTimeToWait, true));
                }
                catch
                {
                    RemoveRegistrationHandle(registrationHandle);

                    throw;
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="commandToRun"></param>
        /// <param name="amountOfTimeToWaitBeforeRunningCommand"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void RegisterDelayedCommand(Command commandToRun, TimeSpan amountOfTimeToWaitBeforeRunningCommand)
        {
            lock (_stateChangeCriticalSection)
            {
                ThrowIfDisposed();

                if (amountOfTimeToWaitBeforeRunningCommand < TimeSpan.Zero)
                {
                    throw new ArgumentOutOfRangeException("amountOfTimeToWaitBeforeRunningCommand", Resources.NegativeTimeSpansNotAllowed);
                }

                this.RegisterCommandWaitingForHandleSignal(_neverSignallingHandle, commandToRun,
                                                           amountOfTimeToWaitBeforeRunningCommand);
            }
        }

        private void AddRegistrationHandle(RegistrationHandle registrationHandle)
        {
            lock (_registrationHandles)
            {
                _registrationHandles.Add(registrationHandle);
            }
        }

        /// <summary>
        /// Note: Does not throw if the handle has not been registered
        /// </summary>
        /// <param name="registrationHandle"></param>
        private void RemoveRegistrationHandle(RegistrationHandle registrationHandle)
        {
            lock (_registrationHandles)
            {
                _registrationHandles.Remove(registrationHandle);
            }
        }

        private void QueueCommandToPool(Command command)
        {
            Interlocked.Increment(ref _enqueuedCommandsCount);

            try
            {
                ThreadPool.UnsafeQueueUserWorkItem(
                    delegate
                        {
                            try
                            {
                                command.Execute();
                            }
                            finally
                            {
                                Interlocked.Decrement(ref _enqueuedCommandsCount);
                            }
                        }, null);
            }
            catch (Exception)
            {
                Interlocked.Decrement(ref _enqueuedCommandsCount);
                throw;
            }
        }

        private void ThrowIfDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(Resources.CommandProcessorDisposed);
            }
        }

        /// <summary>
        /// Note: when this returns, some RegistrationWaitCallbacks may still be in progress.
        /// </summary>
        public void Dispose()
        {
            lock (_stateChangeCriticalSection)
            {
                if (_disposed)
                {
                    return;
                }

                _disposed = true;

                lock (_registrationHandles)
                {
                    foreach (RegistrationHandle registrationHandle in _registrationHandles)
                    {
                        registrationHandle.Unregister();
                    }

                    _registrationHandles.Clear();
                }

                _neverSignallingHandle.Dispose();

                // Note: this can be done more efficiently (target of opportunity for some next release)
                while (Interlocked.CompareExchange(ref _enqueuedCommandsCount, 0, 0) != 0)
                {
                    Thread.Sleep(TimeSpan.FromMilliseconds(200));
                }
            }
        }
    }
}